winbrew_app\operations\install/
download.rs

1//! Download and verification helpers for installer payloads.
2//!
3//! This module owns the network-specific half of the install flow. It creates
4//! the dedicated installer HTTP client, streams the selected installer into a
5//! temporary file, and finalizes the file only after checksum verification has
6//! passed.
7//!
8//! The higher-level orchestration code uses these helpers as a single phase with
9//! well-defined cleanup behavior: temporary files are removed on failure and the
10//! caller receives any tolerated legacy checksum algorithms for reporting.
11
12use anyhow::Result;
13use std::path::Path;
14
15use crate::core::cancel::check;
16use crate::core::fs::{cleanup_path, finalize_temp_file};
17use crate::core::hash::{Hasher, verify_hash};
18use crate::core::network::{build_client as network_build_client, download_url_to_temp_file};
19use crate::models::catalog::CatalogInstaller;
20use crate::models::domains::shared::HashAlgorithm;
21
22const CATALOG_USER_AGENT: &str = "winbrew-package-installer";
23
24/// Build the HTTP client used for installer downloads.
25///
26/// A dedicated user agent makes installer traffic easy to identify in server
27/// logs and keeps the install pipeline separate from catalog refresh traffic.
28pub fn build_client() -> Result<crate::core::network::Client> {
29    Ok(network_build_client(CATALOG_USER_AGENT)?)
30}
31
32/// Download an installer into a temporary file and verify it before finalizing.
33///
34/// The payload is streamed to a `.part` file next to `download_path`, with
35/// progress forwarded through the provided callbacks. If the installer hash is
36/// present, it is verified as the bytes arrive. On success, the temporary file
37/// is atomically finalized into `download_path` and the set of tolerated legacy
38/// checksum algorithms is returned to the caller.
39///
40/// When any step fails, the temporary file is removed so the install flow does
41/// not leave behind partially downloaded payloads.
42pub fn download_installer<FStart, FProgress>(
43    client: &crate::core::network::Client,
44    installer: &CatalogInstaller,
45    download_path: &Path,
46    ignore_checksum_security: bool,
47    on_start: FStart,
48    mut on_progress: FProgress,
49) -> Result<Vec<HashAlgorithm>>
50where
51    FStart: FnOnce(Option<u64>),
52    FProgress: FnMut(u64),
53{
54    let temp_path = download_path.with_extension("part");
55    let download_result = (|| -> Result<Vec<HashAlgorithm>> {
56        check()?;
57
58        let (verification, legacy_checksum_algorithms) = verify_strategy(
59            &installer.hash,
60            installer.hash_algorithm,
61            ignore_checksum_security,
62        )?;
63        let mut verification = verification;
64
65        check()?;
66
67        download_url_to_temp_file(
68            client,
69            &installer.url,
70            &temp_path,
71            "installer",
72            on_start,
73            &mut on_progress,
74            |chunk| {
75                check()?;
76                verification.update(chunk);
77                Ok(())
78            },
79        )?;
80
81        verification.finish(&installer.hash)?;
82
83        finalize_temp_file(&temp_path, download_path)?;
84
85        Ok(legacy_checksum_algorithms)
86    })();
87
88    if download_result.is_err() {
89        let _ = cleanup_path(&temp_path);
90    }
91
92    download_result
93}
94
95enum Verification {
96    None,
97    Active(Box<Hasher>),
98}
99
100impl Verification {
101    fn update(&mut self, chunk: &[u8]) {
102        match self {
103            Self::None => {}
104            Self::Active(hasher) => hasher.update(chunk),
105        }
106    }
107
108    fn finish(self, expected_hash: &str) -> Result<()> {
109        match self {
110            Self::None => Ok(()),
111            Self::Active(hasher) => {
112                verify_hash(expected_hash, hasher.finalize()).map_err(Into::into)
113            }
114        }
115    }
116}
117
118fn verify_strategy(
119    expected_hash: &str,
120    hash_algorithm: HashAlgorithm,
121    ignore_checksum_security: bool,
122) -> Result<(Verification, Vec<HashAlgorithm>)> {
123    let trimmed = expected_hash.trim();
124
125    if trimmed.is_empty() {
126        return Ok((Verification::None, Vec::new()));
127    }
128
129    match hash_algorithm {
130        // MD5 is cryptographically broken beyond use; skip verification entirely
131        // rather than computing a hash we cannot trust.
132        HashAlgorithm::Md5 if ignore_checksum_security => {
133            Ok((Verification::None, vec![HashAlgorithm::Md5]))
134        }
135        HashAlgorithm::Md5 => Err(crate::core::HashError::LegacyChecksumAlgorithm {
136            algorithm: HashAlgorithm::Md5,
137        }
138        .into()),
139        HashAlgorithm::Sha1 if ignore_checksum_security => Ok((
140            Verification::Active(Box::new(Hasher::new(HashAlgorithm::Sha1))),
141            vec![HashAlgorithm::Sha1],
142        )),
143        HashAlgorithm::Sha1 => Err(crate::core::HashError::LegacyChecksumAlgorithm {
144            algorithm: HashAlgorithm::Sha1,
145        }
146        .into()),
147        algorithm => Ok((
148            Verification::Active(Box::new(Hasher::new(algorithm))),
149            Vec::new(),
150        )),
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::{Verification, verify_strategy};
157    use crate::core::HashError;
158    use crate::models::domains::shared::HashAlgorithm;
159
160    #[test]
161    fn verify_strategy_rejects_md5_without_ignore_flag() {
162        let err = match verify_strategy("abc123", HashAlgorithm::Md5, false) {
163            Ok(_) => panic!("md5 should be rejected by default"),
164            Err(err) => err,
165        };
166
167        assert!(matches!(
168            err.downcast_ref::<HashError>(),
169            Some(HashError::LegacyChecksumAlgorithm {
170                algorithm: HashAlgorithm::Md5
171            })
172        ));
173    }
174
175    #[test]
176    fn verify_strategy_tolerates_md5_with_ignore_flag() {
177        let (verification, legacy_checksum_algorithms) =
178            verify_strategy("abc123", HashAlgorithm::Md5, true)
179                .expect("md5 should be tolerated when ignoring checksum security");
180
181        assert!(matches!(verification, Verification::None));
182        assert_eq!(legacy_checksum_algorithms, vec![HashAlgorithm::Md5]);
183    }
184
185    #[test]
186    fn verify_strategy_skips_verification_for_empty_hash() {
187        let (verification, legacy_checksum_algorithms) =
188            verify_strategy("   ", HashAlgorithm::Sha256, false)
189                .expect("empty hashes should bypass verification");
190
191        assert!(matches!(verification, Verification::None));
192        assert!(legacy_checksum_algorithms.is_empty());
193    }
194
195    #[test]
196    fn verify_strategy_still_verifies_sha1_when_ignored() {
197        let (verification, legacy_checksum_algorithms) =
198            verify_strategy("abc123", HashAlgorithm::Sha1, true)
199                .expect("sha1 should remain verifiable when security checks are ignored");
200
201        assert!(matches!(verification, Verification::Active(_)));
202        assert_eq!(legacy_checksum_algorithms, vec![HashAlgorithm::Sha1]);
203    }
204}